package com.googlecode.perfectable.visuable.parse;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.CLASS_ATTRIBUTE_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.CONTROLLER_ELEMENT_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.EVENT_TYPE_ATTRIBUTE_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.FRAMEWORK_NAMESPACE;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.INJECT_VISUAL_ELEMENT_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.LISTENER_ELEMENT_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.PROPERTY_NAME_ATTRIBUTE_NAME;
import static com.googlecode.perfectable.visuable.parse.DocumentConstants.PROPERTY_REFERENCE_ATTRIBUTE_NAME;

import java.util.LinkedList;
import java.util.List;

import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.googlecode.perfectable.eventable.routing.Sink;
import com.googlecode.perfectable.introspection.Classes;
import com.googlecode.perfectable.prosperable.message.ServerMessage;
import com.googlecode.perfectable.visuable.configuration.Configurator;
import com.googlecode.perfectable.visuable.event.ButtonPushEvent;
import com.googlecode.perfectable.visuable.event.Event;
import com.googlecode.perfectable.visuable.event.EventListener;
import com.googlecode.perfectable.visuable.event.UnsupportedEvent;
import com.googlecode.perfectable.visuable.structure.Attribute;
import com.googlecode.perfectable.visuable.structure.Elements;
import com.googlecode.perfectable.visuable.visual.EventHandlingVisual;
import com.googlecode.perfectable.visuable.visual.NotifyingVisualProxy;
import com.googlecode.perfectable.visuable.visual.Visual;
import com.googlecode.perfectable.visuable.visual.controller.Controller;

// MARK refactor!
class StandardConfigurationBuilder {
	private final Configurator configurator;
	private final Sink<ServerMessage> sink;
	private final List<VisualInjection> injections = new LinkedList<>();

	public StandardConfigurationBuilder(Configurator configurator, Sink<ServerMessage> sink) {
		this.configurator = configurator;
		this.sink = sink;
	}

	public void buildConfiguration(Visual visual, Iterable<Node> childNodes) {
		for(Node childNode : childNodes) {
			if(!(childNode instanceof Element)) {
				continue;
			}
			Element childElement = (Element) childNode;
			final String namespaceURI = childElement.getNamespaceURI();
			if(!FRAMEWORK_NAMESPACE.equals(namespaceURI)) {
				continue;
			}
			String localName = childElement.getLocalName();
			if(CONTROLLER_ELEMENT_NAME.equals(localName)) {
				this.createController(childElement, visual); // MARK
			}
			if(LISTENER_ELEMENT_NAME.equals(localName)) {
				this.createListener(childElement, visual); // MARK
			}
		}
	}

	private static class VisualInjection {
		private final Object target;
		private final String propertyName;
		private final String referenceName;

		private VisualInjection(Object target, String propertyName, String referenceName) {
			this.target = checkNotNull(target);
			this.propertyName = checkNotNull(propertyName);
			this.referenceName = checkNotNull(referenceName);
		}

		public static VisualInjection create(Object target, String propertyName, String referenceName) {
			return new VisualInjection(target, propertyName, referenceName);
		}

		public void perform(Configurator configurator, Sink<ServerMessage> sink, VisualRegistry registry) {
			Visual visual = registry.get(this.referenceName);
			if(visual == null) {
				throw new RuntimeException(); // MARK
			}
			Visual notifyingProxy = NotifyingVisualProxy.create(visual, sink);
			configurator.injectVisual(this.target, this.propertyName, notifyingProxy);
		}

	}

	public void performInjections(VisualRegistry registry) {
		for(VisualInjection injection : this.injections) {
			injection.perform(this.configurator, this.sink, registry);
		}
		this.injections.clear();
	}

	private static void checkControllerRuntimeCapability(Controller<?> controller, Visual visual) {
		Class<?> acceptedVisualClass = Classes.getInterfaceArgumentBound(controller.getClass(), Controller.class, 0);
		if(!acceptedVisualClass.isInstance(visual)) {
			throw new IllegalArgumentException("Controller " + controller + " of class " + controller.getClass().getName()
					+ " accepts visuals with upper bound " + acceptedVisualClass
					+ " but was about to be given control of visual with class " + visual.getClass().getName());
		}
	}

	public <C extends Visual, T extends Controller<C>> void createController(Element element, C visual) {
		List<Attribute> attributes = Attribute.convert(element.getAttributes());
		Class<T> controllerClass = (Class<T>) extractClass(attributes, Controller.class); // MARK cast
		Class<? extends C> visualClass = (Class<? extends C>) visual.getClass(); // MARK cast
		T controller = this.configurator.createController(controllerClass, visualClass);
		checkControllerRuntimeCapability(controller, visual);
		createInjections(controller, Elements.childNodes(element));
		@SuppressWarnings("unchecked")
		C notifyingProxy = (C) NotifyingVisualProxy.create(visual, this.sink);
		// cast is safe, checked with #checkControllerRuntimeCapability
		controller.takeControl(notifyingProxy);
	}

	public <E extends Event, T extends EventListener<E>> void createListener(Element element, Visual visual) {
		List<Attribute> attributes = Attribute.convert(element.getAttributes());
		Class<E> eventClass = (Class<E>) selectEvent(attributes); // MARK cast
		Class<T> listenerClass = (Class<T>) extractClass(attributes, EventListener.class); // MARK cast
		T listener = this.configurator.createListener(listenerClass, eventClass);
		createInjections(listener, Elements.childNodes(element));
		if(visual instanceof EventHandlingVisual) {
			try {
				((EventHandlingVisual) visual).registerEventListener(eventClass, listener);
			}
			catch(UnsupportedEvent e) {
				throw new RuntimeException(e); // TODO Auto-generated catch block
			}
		}
	}

	protected static <T> Class<? extends T> extractClass(List<Attribute> attributes, Class<T> expectedType) {
		String className = extractAttribute(attributes, CLASS_ATTRIBUTE_NAME);
		Class<?> loadedClass;
		try {
			loadedClass = Classes.load(className);
		}
		catch(ClassNotFoundException e) {
			throw Throwables.propagate(e); // TODO Auto-generated catch block
		}
		Class<? extends T> instanceClass = loadedClass.asSubclass(expectedType);
		return instanceClass;
	}

	protected static String extractAttribute(List<Attribute> attributes, String name) {
		Optional<String> result = Attribute.find(attributes, name);
		if(!result.isPresent()) {
			throw new RuntimeException(); // MARK
		}
		return result.get();
	}

	protected static Class<? extends Event> selectEvent(List<Attribute> attributes) {
		String eventTypeName = extractAttribute(attributes, EVENT_TYPE_ATTRIBUTE_NAME);
		if(ButtonPushEvent.TYPE_NAME.equals(eventTypeName)) {
			return ButtonPushEvent.class;
		}
		throw new RuntimeException(); // TODO Auto-generated catch block
	}

	public void createInjections(Object target, Iterable<Node> nodes) {
		for(Node node : nodes) {
			if(!(node instanceof Element)) {
				continue;
			}
			Element element = (Element) node;
			Elements.checkName(element, INJECT_VISUAL_ELEMENT_NAME, FRAMEWORK_NAMESPACE); // MARK
			List<Attribute> attributes = Attribute.convert(element.getAttributes());
			String propertyName = extractAttribute(attributes, PROPERTY_NAME_ATTRIBUTE_NAME);
			String idref = extractAttribute(attributes, PROPERTY_REFERENCE_ATTRIBUTE_NAME);
			VisualInjection injection = VisualInjection.create(target, propertyName, idref);
			this.injections.add(injection);
		}
	}

}
